﻿using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Utility.Events;
using System;
using Utility.DataStructure;

namespace Utility.TimerSystem
{
	public class TimerEventSchedule
	{
		public ITimer Timer { get; }
		SortedLinkedList<float, TimerEvent> timeline = new SortedLinkedList<float, TimerEvent> ();

		HashSet<TimedEvent> timedEvents = new HashSet<TimedEvent> ();
		HashSet<PeriodicEvent> periodicEvents = new HashSet<PeriodicEvent> ();

		public TimerEventSchedule (ITimer timer)
		{
			Timer = timer;
		}

		public PeriodicEvent AddPeriodic (float interval, float offset)
		{
			var e = new PeriodicEvent ();
			e.Init (this, interval, offset);
			periodicEvents.Add (e);
			timeline.Add (e.TriggerTime, e);
			return e;
		}

		public void Add (float delay, Action action)
		{
			var e = EventPool.Spawn<TimedEvent> ();
			e.Init (this, delay);
			timeline.Add (e.TriggerTime, e);

			timedEvents.Add (e);
			e.Add (action, true);
		}

		public void UpdateEvents ()
		{
			while (timeline.Peak ().Value.CanInoke ())
			{
				var e = timeline.Pop ().Value;
				e.Invoke ();
				if (e.Alive)
				{
					timeline.Add (e.TriggerTime, e);
				}
				else
				{
					if (e is TimedEvent e_timed)
					{
						timedEvents.Remove (e_timed);
						EventPool.Despawn (e);
					}
					else if (e is PeriodicEvent e_periodic)
					{
						periodicEvents.Remove (e_periodic);
					}
				}
			}
		}

		public void Clear ()
		{
			//clear timeline
			timeline.Clear ();
			//remove timed events
			foreach (var e in timedEvents)
			{
				EventPool.Despawn (e);
			}
			//clear and re-init periodic events
			foreach (var e in periodicEvents)
			{
				e.Clear ();
				e.Init (this, e.Interval, e.Offset);
				timeline.Add (e.TriggerTime, e);
			}
		}
	}

	public abstract class TimerEvent : Event<TimerEvent>
	{
		public TimerEventSchedule EventSchedule { get; protected set; }
		public ITimer Timer => EventSchedule.Timer;
		public float TriggerTime { get; protected set; }
		public bool Alive { get; protected set; } = false;

		protected bool Init (TimerEventSchedule schedule)
		{
			if (!Alive)
			{
				Alive = true;
				EventSchedule = schedule;
				return true;
			}
			return false;
		}

		public bool CanInoke ()
		{
			return Timer.Time >= TriggerTime;
		}

		public virtual void Invoke ()
		{
			Invoke (this);
		}
	}

	public class PeriodicEvent : TimerEvent
	{
		public float Interval { get; protected set; }
		public float Offset { get; protected set; }

		public void Init (TimerEventSchedule schedule, float interval, float offset)
		{
			if (Init (schedule))
			{
				Interval = interval > 0 ? interval : throw new ArgumentOutOfRangeException ();
				Offset = offset;
				var lastTimepoint = (int)(Timer.Time / interval) * interval - offset;
				TriggerTime = lastTimepoint + interval;
			}
		}

		public override void Invoke ()
		{
			base.Invoke ();
			TriggerTime += Interval;
		}
	}

	public class TimedEvent : TimerEvent
	{
		public void Init (TimerEventSchedule schedule, float delay)
		{
			if (Init (schedule))
			{
				TriggerTime = Timer.Time + delay;
			}
		}

		public override void Invoke ()
		{
			base.Invoke ();
			Alive = false;
		}
	}
}